package risoe.syslab.fhnew.hw;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import risoe.syslab.fhnew.common.SystemConstants.DeviceType;
import risoe.syslab.fhnew.config.ActuatorConfig;
import risoe.syslab.fhnew.config.SensorConfig;
import risoe.syslab.fhnew.controller.ControllerManager;
import risoe.syslab.model.CompositeMeasurement;

/**
 * The central data caching facility for the PowerFlexhouse Platform. Serves as a link between the HWModule(and thus HardwareInterfaces)
 * and the ControllerManager. The ProcessImage reflects the last state of all sensors and actuators, and is also used for logging.
 * @author M. Svendsen
 */
public class ProcessImage{
	private Map<String, Sensor> sensors;
	private Map<String,Actuator> actuators;
	private HWModule hwModule;
	private ControllerManager controllerManager;
	private boolean locked;

	// Concurrent Operations
	public static final int THREAD_COUNT = 1;
	private final ExecutorService mExecutor = new ThreadPoolExecutor(THREAD_COUNT, THREAD_COUNT, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

	/**
	 * Creates a new ProcessImage based on the data contained in a HWModule
	 * @param module the HWModule containing the data for use with this ProcessImage
	 */
	public ProcessImage(HWModule module) {
		sensors=new HashMap<String,Sensor>();
		actuators=new HashMap<String,Actuator>();
		hwModule = module;
		locked=false;
	}

	/**
	 * Shuts down the ProcessImage. Should be called in order to halt execution of the associated thread pool gracefully.
	 */
	public void shutdown(){
		mExecutor.shutdownNow();
	}

	/**
	 * Associates a ControllerManager with this ProcessImage
	 * @param manager the ControllerManager to associate
	 */
	public void registerControllerManager(ControllerManager manager) {
		controllerManager = manager;
	}

	/**
	 * Locks the ProcessImage by creating immutable copies of all added sensors and actuators.
	 */
	public void lockImage() {
		locked=true;

		// Create defensive immutable copies of the sensor and actuator lists
		sensors = Collections.unmodifiableMap(sensors);
		actuators = Collections.unmodifiableMap(actuators);
	}

	/**
	 * Adds a new Sensor to the ProcessImage based on a SensorConfig
	 * @param sc the SensorConfig from which a Sensor should be instantiated
	 */
	public void addSensor(SensorConfig sc) {
		if (locked) {
			throw new UnsupportedOperationException("Process image is locked. No new sensors can be added.");
		}
		if (sensors.containsKey(sc.getName())) {
			throw new IllegalArgumentException("Process image already contains a sensor named '"+sc.getName()+"'.");
		}
		sensors.put(sc.getName(),new Sensor(sc));
	}

	/**
	 * Adds a new Actuator to the ProcessImage based on an ActuatorConfig
	 * @param sc the ActuatorConfig from which a Actuator should be instantiated
	 */
	public void addActuator(ActuatorConfig ac) {
		if (locked) {
			throw new UnsupportedOperationException("Process image is locked. No new sensors can be added.");
		}
		if (actuators.containsKey(ac.getName())) {
			throw new IllegalArgumentException("Process image already contains an actuator named '"+ac.getName()+"'.");
		}
		actuators.put(ac.getName(),new Actuator(ac));
	}

	/**
	 * Updates the value of a given Sensor in the ProcessImage. Notifies the ControllerManager of this
	 * update asynchronously.
	 * @param name the name of the Sensor to update
	 * @param value a CompositeMeasurement representing the last measurement received from this Sensor
	 */
	private void updateSensor(String name, CompositeMeasurement value) {
		if (value == null) {
			throw new IllegalArgumentException("Null was passed as a CompositeMeasurement for sensor: "+name);
		}
		final Sensor s=sensors.get(name);
		if (s==null) {
			throw new IllegalArgumentException("No sensor named '"+name+"' in process image.");
		} 
		s.update(value);

		// Run asynchronously on a thread from the pool of worker threads
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				if (controllerManager != null){
					try {
						controllerManager.sensorUpdated(s);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		};

		if (!mExecutor.isShutdown()){
			mExecutor.execute(runnable);
		}
	}

	/**
	 * Updates the value of a given Actuator in the ProcessImage. Notifies the ControllerManager of this
	 * update asynchronously.
	 * @param name the name of the Actuator to update
	 * @param value a CompositeMeasurement representing the last received actuation value
	 */
	public void updateActuator(String name, CompositeMeasurement value) {
		if (value == null) {
			throw new IllegalArgumentException("Null was passed as a CompositeMeasurement for actuator: "+name);
		}
		final Actuator a=actuators.get(name);
		if (a==null) {
			throw new IllegalArgumentException("No actuator named '"+name+"' in process image.");
		}
		a.updateValue(value);

		// Run asynchronously on a thread from the pool of worker threads
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				if (controllerManager != null){
					try {
						controllerManager.actuatorUpdated(a);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		};

		if (!mExecutor.isShutdown()){
			mExecutor.execute(runnable);
		}
	}

	/**
	 * Updates the setpoint of a given Actuator in the ProcessImage. Notifies the HWModule of this
	 * update asynchronously.
	 * @param name the name of the Actuator to update
	 * @param value a CompositeMeasurement representing the last specified setpoint
	 */
	public void setActuator(String name, CompositeMeasurement value) {
		if (value == null) {
			throw new IllegalArgumentException("Null was passed as a setpoint for actuator: "+name);
		}

		final Actuator a=actuators.get(name);
		if (a==null) {
			throw new IllegalArgumentException("No actuator named '"+name+"' in process image.");
		}
		a.updateSetpoint(value);

		// Run asynchronously on a thread from the pool of worker threads
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				// Pass the update instruction to the HWModule
				try {
					hwModule.handleActuation(a);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		};

		if (!mExecutor.isShutdown()){
			mExecutor.execute(runnable);
		}
	}

	/**
	 * Updates the ProcessImage with all the data contained in the passed HashMap. This function is called by
	 * HardwareInterfaces via the HWModule. Notifies the ControllerManager of this update asynchronously.
	 * @param data a HashMap containing device names as keys and CompositeMeasurements as values.
	 */
	public void newData(HashMap<String, CompositeMeasurement> data) {
		for (String sn:data.keySet()) {
			try {
				if (sensors.containsKey(sn)) {
					updateSensor(sn,data.get(sn));
				}
				else if (actuators.containsKey(sn)){
					updateActuator(sn, data.get(sn));
				}
				else {
					System.out.println("Warning: No sensor or actuator named '"+sn+"' in process image. This should never happen.");
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	// Utility Methods

	public DeviceType getActuatorType(String name){
		if (name != null){
			Actuator a = actuators.get(name);
			if (a != null){
				return a.getType();
			}
		}
		return null;
	}

	public CompositeMeasurement getSensorValue(String name) {
		if (name != null){
			Sensor s = sensors.get(name);

			if (s != null){
				// DISABLED FOR NOW - Easily causes overload of KNX Interface


				// Try polling this device using its HardwareInterface
				//CompositeMeasurement meas = hwModule.attemptRead(s);
				//if (meas != null){
				//	return meas;
				//}

				// Polling was not possible - return the last stored value
				return s.getValue();
			}
		}
		return null;
	}

	public CompositeMeasurement getActuatorValue(String name) {
		if (name != null){
			Actuator a = actuators.get(name);
			if (a != null){
				return a.getValue();
			}
		}
		return null;
	}

	public CompositeMeasurement getActuatorSetpoint(String name) {
		if (name != null){
			Actuator a = actuators.get(name);
			if (a != null){
				return a.getSetpoint();
			}
		}
		return null;
	}

	/**
	 * Convenience methods
	 */

	public Sensor[] getSensors() {
		return sensors.values().toArray(new Sensor[0]);
	}

	public Actuator[] getActuators() {
		return actuators.values().toArray(new Actuator[0]);
	}

	public Sensor getSensorByName(String name) {
		return sensors.get(name);
	}

	public Actuator getActuatorByName(String name) {
		return actuators.get(name);
	}
}
